feat: replace homepage frameworks with fun rotating "n p m x" picks#1616
feat: replace homepage frameworks with fun rotating "n p m x" picks#1616
Conversation
Show 4 randomly-selected packages whose names contain the letters n, p, m, X, with the matching
letter highlighted in bold + accent colour (one distinct colour per letter when no user accent is
set).
Selection algorithm:
1. Fetch top 500 popular packages from the Algolia search index (empty query, default popularity
ranking). Algolia doesn't support filtering by name substring (and doesn't know about our social
likes), so we filter these results down after the fact (not deprecated, >=10k downloads/30d,
modified <2yrs).
2. For each letter (n, p, m, x)
1. Take 30 random candidates whose name contains that letter and check their social like count.
2. If there are candidates with >=5 community likes, keep only those; otherwise, keep all.
3. Randomly pick one remaining candidate.
4. If there are no remaining candidates, pick the hardcoded default for this letter (nuxt, pnpm,
module-replacements, oxfmt).
Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and
no user ever experiences a cache miss (and Algolia/constellation slowness).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Since 4 packages are displayed, I think the original width of the wrapper could be restored (#1591 reduced max-w to wrap the long list) |
📝 WalkthroughWalkthroughReplaces the homepage static showcase with a dynamic "npmx picks" feature. Adds server/api/picks.get.ts (cached endpoint querying Algolia, sampling/filtering candidates, enriching with totalLikes, returning one pick per NPMX letter), server/utils/picks.ts (selection logic, thresholds, fallbacks), shared/types/picks.ts and re-export, updates app/pages/index.vue to fetch and render picks, adds unit and e2e tests and Algolia mock fixtures, renames i18n keys/schema from nav.popular_packages → nav.npmx_picks, and changes root route to ISR (3600s). Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
modules/runtime/server/cache.ts (1)
249-257: Consider stabilising mock timestamps for deterministic fixtures.Using one shared timestamp value in this block avoids per-item clock drift and keeps fixture output steadier.
♻️ Suggested tweak
if (host === algoliaHost && pathname.endsWith('/query')) { + const now = new Date().toISOString() return { data: { hits: [ - { name: 'nuxt', downloadsLast30Days: 500_000, modified: new Date().toISOString() }, - { name: 'pnpm', downloadsLast30Days: 800_000, modified: new Date().toISOString() }, - { name: 'express', downloadsLast30Days: 1_000_000, modified: new Date().toISOString() }, - { name: 'minimatch', downloadsLast30Days: 600_000, modified: new Date().toISOString() }, - { name: 'next', downloadsLast30Days: 700_000, modified: new Date().toISOString() }, - { name: 'axios', downloadsLast30Days: 900_000, modified: new Date().toISOString() }, - { name: 'remix', downloadsLast30Days: 400_000, modified: new Date().toISOString() }, - { name: 'webpack', downloadsLast30Days: 750_000, modified: new Date().toISOString() }, + { name: 'nuxt', downloadsLast30Days: 500_000, modified: now }, + { name: 'pnpm', downloadsLast30Days: 800_000, modified: now }, + { name: 'express', downloadsLast30Days: 1_000_000, modified: now }, + { name: 'minimatch', downloadsLast30Days: 600_000, modified: now }, + { name: 'next', downloadsLast30Days: 700_000, modified: now }, + { name: 'axios', downloadsLast30Days: 900_000, modified: now }, + { name: 'remix', downloadsLast30Days: 400_000, modified: now }, + { name: 'webpack', downloadsLast30Days: 750_000, modified: now }, ], }, } }
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (66)
app/pages/index.vuei18n/locales/ar.jsoni18n/locales/az-AZ.jsoni18n/locales/bg-BG.jsoni18n/locales/bn-IN.jsoni18n/locales/cs-CZ.jsoni18n/locales/de-DE.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr-FR.jsoni18n/locales/hi-IN.jsoni18n/locales/hu-HU.jsoni18n/locales/id-ID.jsoni18n/locales/it-IT.jsoni18n/locales/ja-JP.jsoni18n/locales/kn-IN.jsoni18n/locales/mr-IN.jsoni18n/locales/nb-NO.jsoni18n/locales/ne-NP.jsoni18n/locales/pl-PL.jsoni18n/locales/pt-BR.jsoni18n/locales/ru-RU.jsoni18n/locales/ta-IN.jsoni18n/locales/te-IN.jsoni18n/locales/uk-UA.jsoni18n/locales/zh-CN.jsoni18n/locales/zh-TW.jsoni18n/schema.jsonlunaria/files/ar-EG.jsonlunaria/files/az-AZ.jsonlunaria/files/bg-BG.jsonlunaria/files/bn-IN.jsonlunaria/files/cs-CZ.jsonlunaria/files/de-DE.jsonlunaria/files/en-GB.jsonlunaria/files/en-US.jsonlunaria/files/es-419.jsonlunaria/files/es-ES.jsonlunaria/files/fr-FR.jsonlunaria/files/hi-IN.jsonlunaria/files/hu-HU.jsonlunaria/files/id-ID.jsonlunaria/files/it-IT.jsonlunaria/files/ja-JP.jsonlunaria/files/kn-IN.jsonlunaria/files/mr-IN.jsonlunaria/files/nb-NO.jsonlunaria/files/ne-NP.jsonlunaria/files/pl-PL.jsonlunaria/files/pt-BR.jsonlunaria/files/ru-RU.jsonlunaria/files/ta-IN.jsonlunaria/files/te-IN.jsonlunaria/files/uk-UA.jsonlunaria/files/zh-CN.jsonlunaria/files/zh-TW.jsonmodules/runtime/server/cache.tsnuxt.config.tsserver/api/picks.get.tsserver/utils/atproto/utils/likes.tsserver/utils/picks.tsshared/types/index.tsshared/types/picks.tstest/e2e/homepage-picks.spec.tstest/fixtures/mock-routes.cjstest/unit/server/utils/picks.spec.ts
💤 Files with no reviewable changes (47)
- lunaria/files/pl-PL.json
- i18n/locales/pl-PL.json
- lunaria/files/zh-TW.json
- lunaria/files/zh-CN.json
- lunaria/files/bg-BG.json
- lunaria/files/mr-IN.json
- i18n/locales/pt-BR.json
- i18n/locales/ar.json
- i18n/locales/uk-UA.json
- i18n/locales/es.json
- i18n/locales/zh-CN.json
- lunaria/files/bn-IN.json
- lunaria/files/ne-NP.json
- i18n/locales/ja-JP.json
- i18n/locales/mr-IN.json
- lunaria/files/az-AZ.json
- i18n/locales/ta-IN.json
- i18n/locales/bg-BG.json
- i18n/locales/hi-IN.json
- i18n/locales/cs-CZ.json
- i18n/locales/de-DE.json
- i18n/locales/nb-NO.json
- lunaria/files/uk-UA.json
- lunaria/files/ru-RU.json
- i18n/locales/ru-RU.json
- lunaria/files/te-IN.json
- lunaria/files/id-ID.json
- i18n/locales/te-IN.json
- lunaria/files/ta-IN.json
- i18n/locales/az-AZ.json
- lunaria/files/es-ES.json
- i18n/locales/bn-IN.json
- lunaria/files/ja-JP.json
- i18n/locales/ne-NP.json
- lunaria/files/de-DE.json
- lunaria/files/it-IT.json
- lunaria/files/hu-HU.json
- i18n/locales/it-IT.json
- i18n/locales/hu-HU.json
- lunaria/files/ar-EG.json
- lunaria/files/cs-CZ.json
- i18n/locales/zh-TW.json
- lunaria/files/hi-IN.json
- lunaria/files/nb-NO.json
- i18n/locales/id-ID.json
- lunaria/files/es-419.json
- lunaria/files/pt-BR.json
| const PICKS_MAX_AGE_MS = 60 * 60 * 1000 | ||
|
|
||
| /** Pick `n` random items from `arr` (Fisher-Yates on a copy, sliced). */ | ||
| function randomSample<T>(arr: T[], n: number): T[] { |
There was a problem hiding this comment.
Didn't seem worth pulling in a dependency for this. AFAIK this is as simple as it gets without caring about cryptographically secure randomness or anything like that.
There was a problem hiding this comment.
But what if npmx.dev gets hacked because of those picks 🤯
JK of course 😆
Awesome PR btw 🚀
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/fixtures/mock-routes.cjs (1)
571-571:⚠️ Potential issue | 🟠 MajorAlgolia wildcard host pattern will not match correctly.
The pattern
https://*-dsn.algolia.net/**contains a hostname wildcard (*), buturlMatchesPattern()at lines 601-608 only handles/**suffix patterns viastartsWith. The*in the hostname won't be matched, so Algolia requests won't be mocked when usingmatchRoute().🔧 Proposed fix to support wildcard patterns
function urlMatchesPattern(url, pattern) { - // Convert "https://example.com/**" to a prefix check - if (pattern.endsWith('/**')) { - const prefix = pattern.slice(0, -2) - return url.startsWith(prefix) + // Support wildcard patterns (e.g. https://*-dsn.algolia.net/**) + if (pattern.includes('*')) { + const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + return new RegExp(`^${escaped}`).test(url) } return url === pattern }
🧹 Nitpick comments (2)
server/utils/picks.ts (1)
29-52: Selection logic is sound but consider defensive letterIndex handling.The logic correctly handles empty pools via optional chaining and falls back to hardcoded values. However, if a candidate name were to somehow not contain the expected letter (edge case),
indexOfwould return-1.Currently this is safe because:
- Candidates are filtered by
name.toLowerCase().includes(letter)in the API handler- Fallbacks are verified to contain their letters
For defensive coding, you might consider clamping to 0:
🛡️ Optional defensive fix
picks.push({ letter, name, - letterIndex: name.toLowerCase().indexOf(letter), + letterIndex: Math.max(0, name.toLowerCase().indexOf(letter)), })server/api/picks.get.ts (1)
75-96: Type assertion on empty object is fragile.The empty object
{}is cast toRecord<NpmxLetter, PickCandidate[]>, which bypasses type checking. WhilePromise.allensures all letters are assigned before use, this pattern could mask issues if the code is refactored.♻️ Safer initialisation with explicit typing
- const candidatesByLetter = {} as Record<NpmxLetter, PickCandidate[]> + const candidatesByLetter: Partial<Record<NpmxLetter, PickCandidate[]>> = {} await Promise.all( NPMX_LETTERS.map(async letter => { // ... existing code ... candidatesByLetter[letter] = enriched }), ) - const picks = selectPicks(candidatesByLetter) + const picks = selectPicks(candidatesByLetter as Record<NpmxLetter, PickCandidate[]>)This makes the incomplete state explicit during construction, with the cast only at the point of use after all letters are guaranteed to be assigned.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
modules/runtime/server/cache.tsserver/api/picks.get.tsserver/utils/picks.tstest/e2e/homepage-picks.spec.tstest/fixtures/mock-routes.cjstest/unit/server/utils/picks.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- test/unit/server/utils/picks.spec.ts
- test/e2e/homepage-picks.spec.ts
- modules/runtime/server/cache.ts
| function matchAlgoliaApi(_urlString) { | ||
| return json({ | ||
| hits: [ | ||
| { name: 'nuxt', downloadsLast30Days: 500000 }, | ||
| { name: 'pnpm', downloadsLast30Days: 800000 }, | ||
| { name: 'express', downloadsLast30Days: 1000000 }, | ||
| { name: 'minimatch', downloadsLast30Days: 600000 }, | ||
| { name: 'next', downloadsLast30Days: 700000 }, | ||
| { name: 'axios', downloadsLast30Days: 900000 }, | ||
| { name: 'remix', downloadsLast30Days: 400000 }, | ||
| { name: 'webpack', downloadsLast30Days: 750000 }, | ||
| ], | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== 1) Fields in Algolia fixture ==="
cat test/fixtures/mock-routes.cjs | sed -n '527,539p'
echo
echo "=== 2) Full matchAlgoliaApi function context ==="
rg -A 20 "function matchAlgoliaApi" test/fixtures/mock-routes.cjs
echo
echo "=== 3) Fields consumed in picks.get.ts ==="
cat server/api/picks.get.ts
echo
echo "=== 4) Fields consumed in picks.ts ==="
cat server/utils/picks.tsRepository: npmx-dev/npmx.dev
Length of output: 6160
Fixture lacks isDeprecated field required by picks filtering logic.
At lines 527–539, the matchAlgoliaApi mock provides only name and downloadsLast30Days, but the real picks implementation (picks.get.ts) explicitly requests and filters on isDeprecated:
- Line 63:
attributesToRetrieve: ['name', 'downloadsLast30Days', 'isDeprecated'] - Lines 70–75: Filters out deprecated packages with
if (hit.isDeprecated) return false
The fixture never exercises this deprecation-filtering path, leaving regression risk uncovered. All fixture entries will silently pass the deprecation check (as undefined is falsy) regardless of their intended deprecation status.
|
|
||
| it('falls back to non-liked when no liked candidates exist', () => { | ||
| const candidates: Record<NpmxLetter, PickCandidate[]> = { | ||
| n: [{ name: 'nodemon', totalLikes: 2 }], |
There was a problem hiding this comment.
Small nitpick here: But if MIN_LIKES is equal to 2, then this test case seems misleading. I'm not sure if it would be cleaner, but maybe MIN_LIKES - 1 could make this test case more robust? Open for ideas 💡
🔗 Linked issue
😶 discussed on Discord
🧭 Context
📚 Description
Show 4 randomly-selected packages whose names contain the letters n, p, m, x, with the matching letter highlighted in bold + accent colour
(one distinct colour per letter when no user accent is set).Selection algorithm:
Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and no user ever experiences a cache miss (and Algolia/constellation slowness).